﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Ast.Core;
using Ast.Core.Forms;
using Ast.Core.Services;
using Ast.Framework.Caching;
using Ast.Framework.Forms;
using Castle.Core.Logging;
using PacketDotNet;

namespace Ast
{
    [FormMetadata(FormType.Ndis, IsTabForm = true)]
    public partial class NdisForm : Form
    {
        private readonly INdisService _ndisService;
        private readonly IDeviceController _deviceController;
        private readonly INetworkService _networkService;
        private readonly ICacheManager _cacheManager;
        private readonly ISignals _signals;

        private const string NetworkPacketEventName = "Global\\NetworkPacketEvent";
        private const string SignalRealoadConnections = "NdisForm.Connections";
        private readonly IList<ConnectionViewItem> _newPackets = new List<ConnectionViewItem>();

        public NdisForm(INdisService ndisService, IDeviceController deviceController, INetworkService networkService, ICacheManager cacheManager, ISignals signals)
        {
            _ndisService = ndisService;
            _deviceController = deviceController;
            _networkService = networkService;
            _cacheManager = cacheManager;
            _signals = signals;

            Logger = NullLogger.Instance;

            InitializeComponent();
        }

        public ILogger Logger { get; set; }

        private void NdisForm_Load(object sender, EventArgs e)
        {
            lvConnections.ApplyColumnSorter();
        }

        private async void btnHook_Click(object sender, System.EventArgs e)
        {
            await Task.Run(() =>
            {
                _ndisService.AcquireLock();
                _ndisService.Hook();
                _ndisService.ReleaseLock();
            }).ConfigureAwait(false);
        }

        private async void btnListen_Click(object sender, System.EventArgs e)
        {
            EventWaitHandle networkPacketEvent;
            if (!EventWaitHandle.TryOpenExisting(NetworkPacketEventName, EventWaitHandleRights.Synchronize, out networkPacketEvent))
            {
                Logger.Win32ErrorFormat("failed to open network packet event");
                return;
            }

            btnListen.Enabled = false;
            btnListen.Text = "Listening";

            await Task.Run(() =>
            {
                while ((networkPacketEvent.WaitOne()))
                {
                    FetchPacket();
                }
            }).ConfigureAwait(false);
        }

        private void FetchPacket()
        {
            GetNetworkPacketResponse getNetworkPacketResponse;
            if (!_deviceController.Send(IoCtl.IoCtlNetworkGetNetworkPacket, out getNetworkPacketResponse))
            {
                Logger.Error("failed to get network packet");
                return;
            }

            if (getNetworkPacketResponse.Type == -1) // no more
            {
                Invoke(new Action(UpdateConnectionListView)); // update view
                return;
            }
            if (getNetworkPacketResponse.Type == -2) return; // insuffisant buffer size

            // parse packet
            var content = getNetworkPacketResponse.Content.Take((int) getNetworkPacketResponse.Size).ToArray();
            EthernetPacket packet = null;
            try
            {
                packet = Packet.ParsePacket(LinkLayers.Ethernet, content) as EthernetPacket;
            }
            catch (Exception ex)
            {
                Logger.ErrorFormat(ex, "failed to parse packet");
            }
            if (packet == null) return;

            var ipPacket = packet.Extract(typeof (IpPacket)) as IpPacket;
            if (ipPacket != null)
            {
                var tcpPacket = ipPacket.Extract(typeof (TcpPacket)) as TcpPacket;
                if (tcpPacket != null)
                {
                    ProcessPacket(packet, ipPacket, tcpPacket, getNetworkPacketResponse.Type);
                }
            }
        }

        private void ProcessPacket(EthernetPacket ethernetPacket, IpPacket ipPacket, TcpPacket tcpPacket, int type)
        {
            var connections = _cacheManager.Get("connections", context =>
            {
                context.Monitor(_signals.When(SignalRealoadConnections));
                return _networkService.GetTcpConnections().ToList();
            });

            var packetLocalAddress = type == 0 ? ipPacket.SourceAddress : ipPacket.DestinationAddress;
            var packetRemoteAddress = type == 0 ? ipPacket.DestinationAddress : ipPacket.SourceAddress;
            var packetLocalPort = type == 0 ? tcpPacket.SourcePort : tcpPacket.DestinationPort;
            var packetRemotePort = type == 0 ? tcpPacket.DestinationPort : tcpPacket.SourcePort;

            var conn = connections.FirstOrDefault(x => x.LocalAddress.Equals(packetLocalAddress) && x.LocalPort == packetLocalPort);
            if (conn == null)
            {
                _signals.Trigger(SignalRealoadConnections);
                conn = connections.FirstOrDefault(x => x.LocalAddress.Equals(packetLocalAddress) && x.LocalPort == packetLocalPort);
            }
            if (conn == null) return;

            int procId;
            string procName;
            try
            {
                var proc = Process.GetProcessById(conn.ProcessId);
                procId = proc.Id;
                procName = proc.ProcessName;
            }
            catch
            {
                return;
            }

            // record packet
            _newPackets.Add(new ConnectionViewItem
            {
                Pid = procId,
                Name = procName,
                SendPackets = type == 0 ? 1 : 0,
                RecvPackets = type == 1 ? 1 : 0,
                SendBytes = type == 0 ? tcpPacket.BytesHighPerformance.BytesLength : 0,
                RecvBytes = type == 1 ? tcpPacket.BytesHighPerformance.BytesLength : 0,
            });

            // log
            Logger.InfoFormat("process {0}, pid {1}, type {2}, local {3}, remote {4}, size {5}",
                procName,
                procId,
                type == 0 ? "send" : "recv",
                string.Format("{0}:{1}", packetLocalAddress.MapToIPv4(), packetLocalPort),
                string.Format("{0}:{1}", packetRemoteAddress.MapToIPv4(), packetRemotePort),
                tcpPacket.BytesHighPerformance.BytesLength);
        }

        private ListViewItem FindListViewItem(int pid)
        {
            var itemKey = pid.ToString();
            return lvConnections.Items.ContainsKey(itemKey) ? lvConnections.Items[itemKey] : null;
        }

        private void UpdateConnectionListView()
        {
            if (_newPackets.Count == 0) return;

            lvConnections.BeginUpdate();

            foreach (var newPacketViewItem in _newPackets)
            {
                var isNew = false;
                ConnectionViewItem connectionViewItem;
                var listViewItem = FindListViewItem(newPacketViewItem.Pid);
                if (listViewItem == null)
                {
                    isNew = true;
                    connectionViewItem = newPacketViewItem;
                    listViewItem = new ListViewItem(new[]
                    {
                        connectionViewItem.Pid.ToString(),
                        connectionViewItem.Name,
                        string.Empty,
                        string.Empty,
                        string.Empty,
                        string.Empty,
                        string.Empty,
                        string.Empty
                    })
                    {
                        Name = connectionViewItem.Pid.ToString(),
                        Tag = connectionViewItem
                    };
                }
                else
                {
                    connectionViewItem = (ConnectionViewItem) listViewItem.Tag;
                    connectionViewItem.SendPackets += newPacketViewItem.SendPackets;
                    connectionViewItem.RecvPackets += newPacketViewItem.RecvPackets;
                    connectionViewItem.SendBytes += newPacketViewItem.SendBytes;
                    connectionViewItem.RecvBytes += newPacketViewItem.RecvBytes;
                }

                listViewItem.SubItems[2].Text = connectionViewItem.SendPackets.ToString(); // send packets
                listViewItem.SubItems[3].Text = connectionViewItem.RecvPackets.ToString(); // recv packets
                listViewItem.SubItems[4].Text = connectionViewItem.TotalPackets.ToString(); // total packets
                listViewItem.SubItems[5].Text = connectionViewItem.SendBytes.ToString(); // send bytes
                listViewItem.SubItems[6].Text = connectionViewItem.RecvBytes.ToString(); // recv bytes
                listViewItem.SubItems[7].Text = connectionViewItem.TotalBytes.ToString(); // total bytes

                if (isNew)
                {
                    lvConnections.Items.Add(listViewItem);
                }
            }

            lvConnections.Sort();
            lvConnections.AutoResizeColumns();
            lvConnections.EndUpdate();

            _newPackets.Clear(); // clean packets
        }

        private void btnReset_Click(object sender, EventArgs e)
        {
            lvConnections.Items.Clear();
        }

        public class ConnectionViewItem
        {
            public int Pid { get; set; }

            public string Name { get; set; }

            public long SendPackets { get; set; }

            public long RecvPackets { get; set; }

            public long TotalPackets
            {
                get { return SendPackets + RecvPackets; }
            }

            public long SendBytes { get; set; }

            public long RecvBytes { get; set; }

            public long TotalBytes
            {
                get { return SendBytes + RecvBytes; }
            }
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
        public struct GetNetworkPacketResponse
        {
            public int Type;

            public uint Size;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)]
            public byte[] Content;
        }
    }
}